package structures;

import java.util.ArrayList;
import java.util.Random;

import odor.Odor;

import util.Gaussian;
import util.SingleValueGaussian;
import util.Util;

import main.Initializable;
import main.InitializationException;
import main.Main;

/**
 * Represents a network of cells, i.e. the entire EP Layer of the olfactory system.
 * 
 * Contains Columns, which contain Mitral cells and the Granules that are associated with those.
 * 
 * @author pedro
 *
 */
public class Network implements Initializable
{
	private double mitralToColumnConnectivity = 00.20;
	private int granulesPerColumn = 40;
	private boolean dynamicConnectivity = false;
	private Gaussian granuleToMitralWeight = new SingleValueGaussian(1.00);
	private Gaussian granuleActivationThreshold = new SingleValueGaussian(5.00);
	private Gaussian mitralToGranuleWeights = new SingleValueGaussian(1.00);
	
	private ArrayList<Column> columns = null;
	private int size = 0;
	private double[] output = null;
	
	private boolean isInitialized = false;
	
	/**
	 * Creates an empty Network of cells
	 */
	public Network()
	{
		columns = new ArrayList<Column>(main.Main.NUM_COLS);
		size = columns.size();
	}
	
	public Network(int size)
	{
		this.size = size;
		columns = new ArrayList<Column>(size);
	}
	
	//FIXME Make Special Network to handle file input
	public Network(int numCols, int granulesPerColumn,
			double intercolumnConnectivity,
			boolean dynamicItercolumnConnectivity,
			Gaussian granuleToMitralWeight,
			Gaussian granuleActivationThreshold, Gaussian mitralToGranuleWeights) 
	{
		this.columns = new ArrayList<Column>(numCols);
		this.granulesPerColumn = granulesPerColumn;
		this.dynamicConnectivity = dynamicItercolumnConnectivity;
		this.granuleToMitralWeight = granuleToMitralWeight;
		this.granuleActivationThreshold = granuleActivationThreshold;
		this.mitralToGranuleWeights= mitralToGranuleWeights;
		
		this.size = numCols;
	}

	//TODO Network Info Dump
	public void infoDump()
	{
		
	}
	
	@Deprecated
	public double getRandomSampledTheta()
	{
		double value = 0;
		Random r = new Random();
		
		try{
			ArrayList<Granule> temp = this.columns.get(r.nextInt(this.getNumCols())).getGranules();
			value = temp.get(r.nextInt(temp.size())).getMitralWeight();
		}
		catch (Exception e)
		{
			value = 0;
		}
		
		return value;
	}
	
	/**
	 * Calculates the granular sensitivity of this Network's granules (i.e. how 'easy' it is
	 * to make a granule fire)
	 * @return
	 */
	public double getGranularSensitivity()
	{
		double avg = 0, sum = 0;
		
		for (Column c : this.columns)
		{
			sum += c.getAverageGranuleActivationThreshold();
		}
		
		avg = util.Util.truncate(sum/((this.size+0.00)), Main.PRECISION);
		
		return avg;
	}
	
	/**
	 * Gets last-saved numerical Column outputs for this network.
	 * @return
	 */
	public double[] getOutput()
	{
		return this.output;
	}
	
	/**
	 * Fire the entire network given an Odor input
	 * @param odor
	 * @return output of columns fired
	 */
	public double[] fire(Odor odor)
	{
		if (!isInitialized)
			return null;
		
		
		//Overview
		//	Step 1: Mitral Cells send activation signals granules in other columns
		//	Step 2: Granule Cells calculate whether or not they are activated
		//	Step 3: Column outputs are inhibited based on granule activation/weight
		
		//DEBUG
		if(main.Main.VERBOSE)
		{
			System.out.println("\n -ATTEMPTING NETWORK FIRING- \n");
		}
		
		//LOG
		if (Main.LOG_WRITER != null)
		{
			Main.attemptNewLineToLog();
			Main.attemptNewLineToLog();
			Main.attemptWriteToLog(" -ATTEMPTING NETWORK FIRING- ");
			Main.attemptNewLineToLog();
			
			Main.attemptWriteToLog("ODOR INPUT: "+Util.arrayToString(odor.getInputs()));
			Main.attemptNewLineToLog();
		}
		
		output = new double[odor.getInputs().length];
		
		//Step 1: Mitral Cells send activation signals granules in other columns
		for (int x=0; x < this.getNumCols(); x++)
		{
			Mitral m = this.getColumn(x).getMitral();
			double input = odor.getInputs()[x];
			
			for (Column affectedCol : m.getColumnsAffected())
			{
				for(Granule g : affectedCol.getGranules())
				{
					double cumulativeActivation = g.getActivation();
					cumulativeActivation += (input*m.getToGranuleWeight(g));
					g.setActivation(cumulativeActivation);
				}
			}
		}

		//Step 2: Granule Cells calculate whether or not they are activated
		for (int x=0; x < this.getNumCols(); x++)
		{
			for (Granule g : this.getColumn(x).getGranules())
			{
				g.setActivation(g.getActivationThreshold() - g.getActivation());
				g.setActivated(g.getActivation() >= 1 || (g.getActivation() > 0 && Math.round(g.getActivation()) >= 1));
				
				//DEBUG
				if (main.Main.VERBOSE)
				{
					System.out.println("\t"+g.toString() + ((g.isActive()) ? " is ":" is not ") + "activated");
				}
				
				//LOG
				if (Main.LOG_WRITER != null)
				{
					Main.attemptWriteToLog("\t"+g.toString() + ((g.isActive()) ? " is ":" is not ") + "activated");
					Main.attemptNewLineToLog();
				}
			}
		}
		
		//Step 3: Column outputs are inhibited based on granule activation*weight
		for (int x=0; x < this.getNumCols(); x++)
		{
			double input = odor.getInputs()[x];
			
			for (Granule g : this.getColumn(x).getGranules())
			{
				input -= g.getMitralWeight() * ((g.isActive()) ? 1.00:0.00);
			}
			
			output[x] = input;
		}
		
		//DEBUG
		if(main.Main.VERBOSE)
		{
			System.out.print("Output:");
			for (double d : output)
				System.out.print(" "+d+" ");
			System.out.println();
		}
		
		//LOG
		if (Main.LOG_WRITER != null)
		{
			Main.attemptWriteToLog("Output:");
			for (double d : output)
				Main.attemptWriteToLog(" "+d+" ");
			Main.attemptNewLineToLog();
		}
		
		return output;
	}
	
	/**
	 * Resets the activation state of all granules in the Network
	 */
	public void resetGranules()
	{
		for (int x=0; x < this.getNumCols(); x++)
		{
			for (Granule g : this.getColumn(x).getGranules())
			{
				g.setActivation(0.00);
			}
		}
	}
	
	public Column getColumn(int index)
	{
		return columns.get(index);
	}
	
	public int getGranulesPerColumn() 
	{
		return granulesPerColumn;
	}

	public void setGranulesPerColumn(int granulesPerColumn) 
	{
		this.granulesPerColumn = granulesPerColumn;
	}

	public Gaussian getGranuleToMitralWeight() 
	{
		return this.granuleToMitralWeight;
	}

	public double getMitralToColumnConnectivity() 
	{
		return mitralToColumnConnectivity;
	}

	public void setMitralToColumnConnectivity(
			double mitralToColumnConnectivity) 
	{
		this.mitralToColumnConnectivity = mitralToColumnConnectivity;
	}

	public Gaussian getMitralToGranuleWeights() {
		return mitralToGranuleWeights;
	}

	public void setMitralToGranuleWeights(Gaussian mitralToGranuleWeights) {
		this.mitralToGranuleWeights = mitralToGranuleWeights;
	}

	public Gaussian getGranuleActivationThreshold() {
		return granuleActivationThreshold;
	}

	public void setGranuleActivationThreshold(Gaussian granuleActivationThreshold) {
		this.granuleActivationThreshold = granuleActivationThreshold;
	}

	/**
	 * Initializes individual columns within the entire Network.
	 */
	public void initialize(Object arg0) throws InitializationException
	{
		//LOG
		if (Main.LOG_WRITER != null)
		{
			Main.attemptNewLineToLog();
			Main.attemptWriteToLog("NETWORK INITIALIZING...");
			Main.attemptNewLineToLog();
		}
		
		//Step 1: Add columns to list
		for (int x=this.size; x > 0; x--)
		{
			columns.add(new Column(this));
		}
		
		//Step 2: Initialize said columns
		for (Column c: columns)
		{
			if (c.isInitialized() == false)
			{
				c.initialize(this);
			}
		}
		
		this.isInitialized  = true;
	}
	
	public int indexOf(Column c)
	{
		return this.columns.indexOf(c);
	}
	
	/**
	 * Gets number of columns in current network
	 * @return
	 */
	public int getNumCols()
	{
		return size;
	}

	public boolean isInitialized() 
	{
		return this.isInitialized;
	}
}
